/*

    Example sketch for TFT_eSPI library.

    No fonts are needed.

    Draws a 3d rotating cube on the TFT screen.

    Original code was found at http://forum.freetronics.com/viewtopic.php?f=37&t=5495

*/

#define BLACK 0x0000
#define WHITE 0xFFFF

#include <SPI.h>

#include <TFT_eSPI.h> // Hardware-specific library

TFT_eSPI tft = TFT_eSPI();       // Invoke custom library

int16_t h;
int16_t w;

int inc = -2;

float xx, xy, xz;
float yx, yy, yz;
float zx, zy, zz;

float fact;

int Xan, Yan;

int Xoff;
int Yoff;
int Zoff;

struct Point3d {
    int x;
    int y;
    int z;
};

struct Point2d {
    int x;
    int y;
};

int LinestoRender; // lines to render.
int OldLinestoRender; // lines to render just in case it changes. this makes sure the old lines all get erased.

struct Line3d {
    Point3d p0;
    Point3d p1;
};

struct Line2d {
    Point2d p0;
    Point2d p1;
};

Line3d Lines[20];
Line2d Render[20];
Line2d ORender[20];

/***********************************************************************************************************************************/
void setup() {

    tft.init();

    h = tft.height();
    w = tft.width();

    tft.setRotation(1);

    tft.fillScreen(TFT_BLACK);

    cube();

    fact = 180 / 3.14159259; // conversion from degrees to radians.

    Xoff = 240; // Position the center of the 3d conversion space into the center of the TFT screen.
    Yoff = 160;
    Zoff = 550; // Z offset in 3D space (smaller = closer and bigger rendering)
}

/***********************************************************************************************************************************/
void loop() {

    // Rotate around x and y axes in 1 degree increments
    Xan++;
    Yan++;

    Yan = Yan % 360;
    Xan = Xan % 360; // prevents overflow.

    SetVars(); //sets up the global vars to do the 3D conversion.

    // Zoom in and out on Z axis within limits
    // the cube intersects with the screen for values < 160
    Zoff += inc;
    if (Zoff > 500) {
        inc = -1;    // Switch to zoom in
    } else if (Zoff < 160) {
        inc = 1;    // Switch to zoom out
    }

    for (int i = 0; i < LinestoRender ; i++) {
        ORender[i] = Render[i]; // stores the old line segment so we can delete it later.
        ProcessLine(&Render[i], Lines[i]); // converts the 3d line segments to 2d.
    }
    RenderImage(); // go draw it!

    delay(14); // Delay to reduce loop rate (reduces flicker caused by aliasing with TFT screen refresh rate)
}

/***********************************************************************************************************************************/
void RenderImage(void) {
    // renders all the lines after erasing the old ones.
    // in here is the only code actually interfacing with the OLED. so if you use a different lib, this is where to change it.

    for (int i = 0; i < OldLinestoRender; i++) {
        tft.drawLine(ORender[i].p0.x, ORender[i].p0.y, ORender[i].p1.x, ORender[i].p1.y, BLACK); // erase the old lines.
    }


    for (int i = 0; i < LinestoRender; i++) {
        uint16_t color = TFT_BLUE;
        if (i < 4) {
            color = TFT_RED;
        }
        if (i > 7) {
            color = TFT_GREEN;
        }
        tft.drawLine(Render[i].p0.x, Render[i].p0.y, Render[i].p1.x, Render[i].p1.y, color);
    }
    OldLinestoRender = LinestoRender;
}

/***********************************************************************************************************************************/
// Sets the global vars for the 3d transform. Any points sent through "process" will be transformed using these figures.
// only needs to be called if Xan or Yan are changed.
void SetVars(void) {
    float Xan2, Yan2, Zan2;
    float s1, s2, s3, c1, c2, c3;

    Xan2 = Xan / fact; // convert degrees to radians.
    Yan2 = Yan / fact;

    // Zan is assumed to be zero

    s1 = sin(Yan2);
    s2 = sin(Xan2);

    c1 = cos(Yan2);
    c2 = cos(Xan2);

    xx = c1;
    xy = 0;
    xz = -s1;

    yx = (s1 * s2);
    yy = c2;
    yz = (c1 * s2);

    zx = (s1 * c2);
    zy = -s2;
    zz = (c1 * c2);
}


/***********************************************************************************************************************************/
// processes x1,y1,z1 and returns rx1,ry1 transformed by the variables set in SetVars()
// fairly heavy on floating point here.
// uses a bunch of global vars. Could be rewritten with a struct but not worth the effort.
void ProcessLine(struct Line2d* ret, struct Line3d vec) {
    float zvt1;
    int xv1, yv1, zv1;

    float zvt2;
    int xv2, yv2, zv2;

    int rx1, ry1;
    int rx2, ry2;

    int x1;
    int y1;
    int z1;

    int x2;
    int y2;
    int z2;

    int Ok;

    x1 = vec.p0.x;
    y1 = vec.p0.y;
    z1 = vec.p0.z;

    x2 = vec.p1.x;
    y2 = vec.p1.y;
    z2 = vec.p1.z;

    Ok = 0; // defaults to not OK

    xv1 = (x1 * xx) + (y1 * xy) + (z1 * xz);
    yv1 = (x1 * yx) + (y1 * yy) + (z1 * yz);
    zv1 = (x1 * zx) + (y1 * zy) + (z1 * zz);

    zvt1 = zv1 - Zoff;

    if (zvt1 < -5) {
        rx1 = 256 * (xv1 / zvt1) + Xoff;
        ry1 = 256 * (yv1 / zvt1) + Yoff;
        Ok = 1; // ok we are alright for point 1.
    }

    xv2 = (x2 * xx) + (y2 * xy) + (z2 * xz);
    yv2 = (x2 * yx) + (y2 * yy) + (z2 * yz);
    zv2 = (x2 * zx) + (y2 * zy) + (z2 * zz);

    zvt2 = zv2 - Zoff;

    if (zvt2 < -5) {
        rx2 = 256 * (xv2 / zvt2) + Xoff;
        ry2 = 256 * (yv2 / zvt2) + Yoff;
    } else {
        Ok = 0;
    }

    if (Ok == 1) {

        ret->p0.x = rx1;
        ret->p0.y = ry1;

        ret->p1.x = rx2;
        ret->p1.y = ry2;
    }
    // The ifs here are checks for out of bounds. needs a bit more code here to "safe" lines that will be way out of whack, so they dont get drawn and cause screen garbage.

}

/***********************************************************************************************************************************/
// line segments to draw a cube. basically p0 to p1. p1 to p2. p2 to p3 so on.
void cube(void) {
    // Front Face.

    Lines[0].p0.x = -50;
    Lines[0].p0.y = -50;
    Lines[0].p0.z = 50;
    Lines[0].p1.x = 50;
    Lines[0].p1.y = -50;
    Lines[0].p1.z = 50;

    Lines[1].p0.x = 50;
    Lines[1].p0.y = -50;
    Lines[1].p0.z = 50;
    Lines[1].p1.x = 50;
    Lines[1].p1.y = 50;
    Lines[1].p1.z = 50;

    Lines[2].p0.x = 50;
    Lines[2].p0.y = 50;
    Lines[2].p0.z = 50;
    Lines[2].p1.x = -50;
    Lines[2].p1.y = 50;
    Lines[2].p1.z = 50;

    Lines[3].p0.x = -50;
    Lines[3].p0.y = 50;
    Lines[3].p0.z = 50;
    Lines[3].p1.x = -50;
    Lines[3].p1.y = -50;
    Lines[3].p1.z = 50;


    //back face.

    Lines[4].p0.x = -50;
    Lines[4].p0.y = -50;
    Lines[4].p0.z = -50;
    Lines[4].p1.x = 50;
    Lines[4].p1.y = -50;
    Lines[4].p1.z = -50;

    Lines[5].p0.x = 50;
    Lines[5].p0.y = -50;
    Lines[5].p0.z = -50;
    Lines[5].p1.x = 50;
    Lines[5].p1.y = 50;
    Lines[5].p1.z = -50;

    Lines[6].p0.x = 50;
    Lines[6].p0.y = 50;
    Lines[6].p0.z = -50;
    Lines[6].p1.x = -50;
    Lines[6].p1.y = 50;
    Lines[6].p1.z = -50;

    Lines[7].p0.x = -50;
    Lines[7].p0.y = 50;
    Lines[7].p0.z = -50;
    Lines[7].p1.x = -50;
    Lines[7].p1.y = -50;
    Lines[7].p1.z = -50;


    // now the 4 edge lines.

    Lines[8].p0.x = -50;
    Lines[8].p0.y = -50;
    Lines[8].p0.z = 50;
    Lines[8].p1.x = -50;
    Lines[8].p1.y = -50;
    Lines[8].p1.z = -50;

    Lines[9].p0.x = 50;
    Lines[9].p0.y = -50;
    Lines[9].p0.z = 50;
    Lines[9].p1.x = 50;
    Lines[9].p1.y = -50;
    Lines[9].p1.z = -50;

    Lines[10].p0.x = -50;
    Lines[10].p0.y = 50;
    Lines[10].p0.z = 50;
    Lines[10].p1.x = -50;
    Lines[10].p1.y = 50;
    Lines[10].p1.z = -50;

    Lines[11].p0.x = 50;
    Lines[11].p0.y = 50;
    Lines[11].p0.z = 50;
    Lines[11].p1.x = 50;
    Lines[11].p1.y = 50;
    Lines[11].p1.z = -50;

    LinestoRender = 12;
    OldLinestoRender = LinestoRender;

}

